Skip to main content

copp\copp\copp2\opt2/
copp2_socp.rs

1//! 2nd-order Convex-Objective Path Parameterization (COPP2) based on second-order cone programming (SOCP).
2//!
3//! # Method identity
4//! This module implements the **optimization backend** for COPP2 by transforming path-parameterization
5//! constraints/objectives into a Clarabel-compatible conic form and solving it with SOCP.
6//!
7//! # Discrete variables (local notation)
8//! On a path grid `s[0..=n]`:
9//! - `a[k]` denotes $\dot{s}_k^2$ (state variable, expected nonnegative in feasible solutions);
10//! - decision vector is organized as `x = [a[0..=n], x_others]`, where `x_others` are auxiliary variables introduced by objective terms (e.g. reciprocal/soc slack variables);
11//!
12//! # High-level pipeline
13//! 1. Validate interval and boundary consistency.
14//! 2. Estimate capacities and assemble standard TOPP2 constraints.
15//! 3. Add COPP2 objective-induced variables/cones ([`Time`](crate::prelude::CoppObjective::Time), [`ThermalEnergy`](crate::prelude::CoppObjective::ThermalEnergy), [`TotalVariationTorque`](crate::prelude::CoppObjective::TotalVariationTorque), [`Linear`](crate::prelude::CoppObjective::Linear)).
16//! 4. Build sparse matrices `A`, `P`, vector `q`, and solve by Clarabel.
17//! 5. Apply status acceptance policy ([`ClarabelOptions::is_allow`](crate::solver::copp2_socp::ClarabelOptions::is_allow)) and extract `a` when allowed.
18//!
19//! # API layering
20//! - [`copp2_socp`](crate::solver::copp2_socp::copp2_socp): strict/normal API, returns only accepted `a`.
21//! - [`copp2_socp_expert`](crate::solver::copp2_socp::copp2_socp_expert): expert API, always returns full Clarabel solution for diagnosis.
22//! - [`copp2_socp_expert_with_info`](crate::solver::copp2_socp::copp2_socp_expert_with_info): expert API plus Clarabel linear-solver
23//!   metadata for solver-side diagnostics.
24
25use crate::copp::clarabel_backend::{ConstraintsClarabel, ObjConsClarabel};
26use crate::copp::copp2::formulation::Copp2Problem;
27use crate::copp::copp2::opt2::ClarabelExpertInfor2nd;
28use crate::copp::copp2::opt2::clarabel_constraints::{
29    clarabel_standard_capacity_topp2, clarabel_standard_constraint_topp2,
30};
31use crate::copp::{ClarabelOptions, CoppObjective, clarabel_to_copp2_solution};
32use crate::diag::{
33    CoppError, DebugVerboser, SilentVerboser, SummaryVerboser, TraceVerboser, Verboser, Verbosity,
34    format_duration_human,
35};
36use crate::robot::robot_core::{Robot, RobotBasic, RobotTorque};
37use clarabel::algebra::CscMatrix;
38use clarabel::solver::SupportedConeT::{NonnegativeConeT, SecondOrderConeT, ZeroConeT};
39use clarabel::solver::{DefaultSolution, DefaultSolver, IPSolver, SupportedConeT};
40use core::f64;
41use itertools::{Itertools, izip};
42use nalgebra::{DMatrix, DVectorView};
43
44#[cfg(any(feature = "c", feature = "cpp", feature = "python", test))]
45use crate::copp::copp2::stable::basic::a_to_b_topp2;
46
47/// Strict COPP2-SOCP API for production use.
48///
49/// # Purpose
50/// Use this entry when caller only needs a valid trajectory profile `a` and treats
51/// non-accepted solver statuses as hard failures.
52///
53/// # Contract
54/// - Internally calls [`copp2_socp_expert`](crate::solver::copp2_socp::copp2_socp_expert).
55/// - Returns `Ok(a)` **iff** `options.is_allow(solution.status)` is `true`.
56/// - Returns `Err(CoppError::ClarabelSolverStatus(...))` when status is not accepted.
57///
58/// # Returns
59/// Returns accepted profile `a` for production usage.
60///
61/// # Errors
62/// Returns [`CoppError`](crate::diag::CoppError) for model/solver failures and when solver status is not accepted.
63///
64/// # Notes
65/// For workflows requiring low-level diagnostics (`status`, iterate behavior, residual-related fields in
66/// Clarabel solution), prefer [`copp2_socp_expert`](crate::solver::copp2_socp::copp2_socp_expert).
67pub fn copp2_socp<'a, M: RobotTorque>(
68    problem: &Copp2Problem<'a, M>,
69    options: &ClarabelOptions,
70) -> Result<Vec<f64>, CoppError> {
71    let (a_profile, solution) = copp2_socp_expert(problem, options)?;
72    a_profile.ok_or_else(|| CoppError::ClarabelSolverStatus("copp2_socp".into(), solution.status))
73}
74
75/// Expert COPP2-SOCP API with full Clarabel solution exposure.
76///
77/// # Purpose
78/// This API is intended for advanced users who need both:
79/// - extracted high-level profile `Option<Vec<f64>>`, and
80/// - raw solver result [`DefaultSolution<f64>`](clarabel::solver::DefaultSolution) for post-analysis.
81///
82/// # Return contract
83/// - `Ok((Some(a), solution))`: status accepted by `options.is_allow(solution.status)`.
84/// - `Ok((None, solution))`: solve finished but status not accepted by policy.
85/// - `Err(...)`: true runtime failures only (input validation / model build / solver construction).
86///
87/// # Returns
88/// Returns tuple `(Option<Vec<f64>>, DefaultSolution<f64>)` for diagnostic workflows.
89///
90/// # Errors
91/// Returns [`CoppError`](crate::diag::CoppError) only for real failures (input, model build, or solver runtime).
92///
93/// # Contract
94/// - caller must handle `None` profile when status is not accepted;
95/// - acceptance policy is fully controlled by `options.is_allow`.
96///   See [`ClarabelOptions::is_allow`](crate::solver::copp2_socp::ClarabelOptions::is_allow)
97///   for a status-handling example.
98///
99/// # Verbosity behavior
100/// Logging is layered by `options.verbosity()`:
101/// - [`Silent`](crate::diag::Verbosity::Silent): no algorithm logs;
102/// - [`Summary`](crate::diag::Verbosity::Summary): lifecycle milestones and elapsed time;
103/// - [`Debug`](crate::diag::Verbosity::Debug): assembly-level counters and stage summaries;
104/// - [`Trace`](crate::diag::Verbosity::Trace): fine-grained stage deltas and solver snapshot diagnostics.
105pub fn copp2_socp_expert<'a, M: RobotTorque>(
106    problem: &Copp2Problem<'a, M>,
107    options: &ClarabelOptions,
108) -> Result<(Option<Vec<f64>>, DefaultSolution<f64>), CoppError> {
109    let result = copp2_socp_expert_with_info(problem, options)?;
110    Ok((result.result, result.solution))
111}
112
113/// Expert COPP2-SOCP API with Clarabel solution and linear-solver diagnostics.
114///
115/// Use this variant when callers need more than
116/// [`DefaultSolution`](clarabel::solver::DefaultSolution), because Clarabel stores linear solver metadata on the
117/// solver `info` object rather than inside the returned solution.
118///
119/// Status acceptance follows
120/// [`ClarabelOptions::is_allow`](crate::solver::copp2_socp::ClarabelOptions::is_allow);
121/// see that method for the shared status-handling pattern.
122pub fn copp2_socp_expert_with_info<'a, M: RobotTorque>(
123    problem: &Copp2Problem<'a, M>,
124    options: &ClarabelOptions,
125) -> Result<ClarabelExpertInfor2nd, CoppError> {
126    match options.verbosity() {
127        Verbosity::Silent => copp2_socp_core(problem, (options, SilentVerboser)),
128        Verbosity::Summary => copp2_socp_core(problem, (options, SummaryVerboser::new())),
129        Verbosity::Debug => copp2_socp_core(problem, (options, DebugVerboser::new())),
130        Verbosity::Trace => copp2_socp_core(problem, (options, TraceVerboser::new())),
131    }
132}
133
134/// Core implementation for COPP2-SOCP expert flow.
135///
136/// # Internal contract
137/// `options_verboser` packs:
138/// - `options`: acceptance policy and Clarabel numerical settings;
139/// - `verboser`: concrete logger implementation chosen by external verbosity dispatch.
140///
141/// # Invariants
142/// - decision-variable layout always starts with contiguous `a[0..=n]`;
143/// - `q_object.len()` is treated as final `n_var` before solver build;
144/// - extracted `a` is produced only through [`clarabel_to_copp2_solution`](crate::solver::copp2_socp::clarabel_to_copp2_solution) when status is accepted.
145fn copp2_socp_core<'a, M: RobotTorque>(
146    problem: &Copp2Problem<'a, M>,
147    options_verboser: (&ClarabelOptions, impl Verboser),
148) -> Result<ClarabelExpertInfor2nd, CoppError> {
149    let (options, mut verboser) = options_verboser;
150    let (idx_s_start, idx_s_final) = problem.idx_s_interval;
151    if verboser.is_enabled(Verbosity::Summary) {
152        verboser.record_start_time();
153        crate::verbosity_log!(
154            crate::diag::Verbosity::Summary,
155            "\ncopp2_socp started: {} <= idx_s <= {}, objectives = {}, s_len = {}.",
156            idx_s_start,
157            idx_s_final,
158            problem.objectives.len(),
159            problem.s_len()
160        );
161    }
162    if verboser.is_enabled(Verbosity::Trace) {
163        let settings = options.clarabel_settings();
164        crate::verbosity_log!(
165            crate::diag::Verbosity::Summary,
166            "copp2_socp: options snapshot -> allow(almost={}, max_iter={}, max_time={}, callback_term={}, insufficient_progress={}), tol_gap_rel={}, tol_feas={}, max_iter={}, verbose={}",
167            options.is_allow(clarabel::solver::SolverStatus::AlmostSolved),
168            options.is_allow(clarabel::solver::SolverStatus::MaxIterations),
169            options.is_allow(clarabel::solver::SolverStatus::MaxTime),
170            options.is_allow(clarabel::solver::SolverStatus::CallbackTerminated),
171            options.is_allow(clarabel::solver::SolverStatus::InsufficientProgress),
172            settings.tol_gap_rel,
173            settings.tol_feas,
174            settings.max_iter,
175            settings.verbose
176        );
177    }
178    // Check input validity
179    let n = idx_s_final - idx_s_start;
180    // Let x = [a[0], a[1], ..., a[n], x_others] \in R^{n+1+n_others}.
181    // Step 1. Deal with constraints
182    // Step 1.1 Compute the number of constraints
183    let (cap_val_std, cap_b_std, cap_cone_std) =
184        clarabel_standard_capacity_topp2(&problem.robot.constraints, problem.idx_s_interval);
185    let (cap_val_obj, cap_b_obj, cap_cone_obj, n_vars) =
186        clarabel_objective_capacity_copp2(n, problem.objectives, problem.robot);
187    if verboser.is_enabled(Verbosity::Debug) {
188        crate::verbosity_log!(
189            crate::diag::Verbosity::Summary,
190            "copp2_socp: capacity estimate std(val={cap_val_std}, b={cap_b_std}, cone={cap_cone_std}), obj(val={cap_val_obj}, b={cap_b_obj}, cone={cap_cone_obj}), n_vars={n_vars}."
191        );
192    }
193    // s=b-A*x \in cone, where A[row[i],col[i]]=val[i], A \in R^{m*(n+1)}, b \in R^m, s \in R^m
194    // -s=-b+A*x
195    let mut row = Vec::<usize>::with_capacity(cap_val_std + cap_val_obj);
196    let mut col = Vec::<usize>::with_capacity(cap_val_std + cap_val_obj);
197    let mut val = Vec::<f64>::with_capacity(cap_val_std + cap_val_obj);
198    let mut b = Vec::<f64>::with_capacity(cap_b_std + cap_b_obj);
199    let mut cones = Vec::<SupportedConeT<f64>>::with_capacity(cap_cone_std + cap_cone_obj);
200    if verboser.is_enabled(Verbosity::Trace) {
201        crate::verbosity_log!(
202            crate::diag::Verbosity::Summary,
203            "copp2_socp: allocated capacities row/col/val/b/cones <= {}/{}/{}/{}/{}",
204            cap_val_std + cap_val_obj,
205            cap_val_std + cap_val_obj,
206            cap_val_std + cap_val_obj,
207            cap_b_std + cap_b_obj,
208            cap_cone_std + cap_cone_obj
209        );
210    }
211    // Step 1.2 set constraints
212    let row_before_std = row.len();
213    let col_before_std = col.len();
214    let val_before_std = val.len();
215    let b_before_std = b.len();
216    let cones_before_std = cones.len();
217    clarabel_standard_constraint_topp2(
218        &problem.as_topp2_problem(),
219        (&mut row, &mut col, &mut val, &mut b, &mut cones),
220        &verboser,
221    );
222    if verboser.is_enabled(Verbosity::Trace) {
223        crate::verbosity_log!(
224            crate::diag::Verbosity::Summary,
225            "copp2_socp: standard-constraints delta row/col/val/b/cones = +{}/+{}/+{}/+{}/+{}",
226            row.len() - row_before_std,
227            col.len() - col_before_std,
228            val.len() - val_before_std,
229            b.len() - b_before_std,
230            cones.len() - cones_before_std
231        );
232    }
233    // Step 2. set objective
234    // Step 2.1. determine whether eta=1/sqrt(a) is needed.
235    let row_before_sqrt = row.len();
236    let col_before_sqrt = col.len();
237    let val_before_sqrt = val.len();
238    let b_before_sqrt = b.len();
239    let cones_before_sqrt = cones.len();
240    let n_var_old = clarabel_sqrt_a_copp2(
241        n,
242        problem.objectives,
243        problem.a_boundary,
244        (&mut row, &mut col, &mut val, &mut b, &mut cones),
245    );
246    if verboser.is_enabled(Verbosity::Trace) {
247        crate::verbosity_log!(
248            crate::diag::Verbosity::Summary,
249            "copp2_socp: sqrt-a stage delta row/col/val/b/cones = +{}/+{}/+{}/+{}/+{}, n_var_old={}",
250            row.len() - row_before_sqrt,
251            col.len() - col_before_sqrt,
252            val.len() - val_before_sqrt,
253            b.len() - b_before_sqrt,
254            cones.len() - cones_before_sqrt,
255            n_var_old
256        );
257    }
258    let mut q_object = Vec::<f64>::with_capacity(n_vars);
259    q_object.resize(n_var_old, 0.0);
260    // Step 2.2. add constraints and objective for each term in the objective.
261    let row_before_obj = row.len();
262    let col_before_obj = col.len();
263    let val_before_obj = val.len();
264    let b_before_obj = b.len();
265    let cones_before_obj = cones.len();
266    let q_before_obj = q_object.len();
267    clarable_objective_copp2(
268        problem,
269        (
270            &mut row,
271            &mut col,
272            &mut val,
273            &mut b,
274            &mut cones,
275            &mut q_object,
276        ),
277    )?;
278    if verboser.is_enabled(Verbosity::Trace) {
279        let (q_min, q_max) = q_object
280            .iter()
281            .fold((f64::INFINITY, f64::NEG_INFINITY), |(mn, mx), &v| {
282                (mn.min(v), mx.max(v))
283            });
284        crate::verbosity_log!(
285            crate::diag::Verbosity::Summary,
286            "copp2_socp: objective stage delta row/col/val/b/cones/q = +{}/+{}/+{}/+{}/+{}/+{}, q_range=[{}, {}]",
287            row.len() - row_before_obj,
288            col.len() - col_before_obj,
289            val.len() - val_before_obj,
290            b.len() - b_before_obj,
291            cones.len() - cones_before_obj,
292            q_object.len() - q_before_obj,
293            q_min,
294            q_max
295        );
296    }
297    if verboser.is_enabled(Verbosity::Debug) {
298        crate::verbosity_log!(
299            crate::diag::Verbosity::Summary,
300            "copp2_socp: after objective assembly row={}, col={}, val={}, b={}, cones={}, q={}",
301            row.len(),
302            col.len(),
303            val.len(),
304            b.len(),
305            cones.len(),
306            q_object.len()
307        );
308    }
309    if verboser.is_enabled(Verbosity::Summary) {
310        crate::verbosity_log!(
311            crate::diag::Verbosity::Summary,
312            "copp2_socp: ready to solve with row/col/val/b/cones = {}/{}/{}/{}/{} and n_var = {}.",
313            row.len(),
314            col.len(),
315            val.len(),
316            b.len(),
317            cones.len(),
318            q_object.len()
319        );
320    }
321    // Step 2.3 build the constraints
322    let n_var = q_object.len();
323    let a_csc = CscMatrix::new_from_triplets(b.len(), n_var, row, col, val);
324    let p_object = CscMatrix::<f64>::zeros((n_var, n_var));
325    if verboser.is_enabled(Verbosity::Trace) {
326        crate::verbosity_log!(
327            crate::diag::Verbosity::Summary,
328            "copp2_socp: matrix built with m={}, n={}, A.nnz={}, P.nnz={}",
329            b.len(),
330            n_var,
331            a_csc.nnz(),
332            p_object.nnz()
333        );
334    }
335    // Step 3. solve the SOCP problem
336    let settings = options.clarabel_settings().clone();
337    let mut solver = DefaultSolver::<f64>::new(&p_object, &q_object, &a_csc, &b, &cones, settings)
338        .map_err(|e| CoppError::ClarabelSolverError("copp2_socp".into(), e))?;
339    solver.solve();
340    let linsolver = solver.info.linsolver.clone();
341    let solution = solver.solution;
342    if verboser.is_enabled(Verbosity::Summary) {
343        crate::verbosity_log!(
344            crate::diag::Verbosity::Summary,
345            "copp2_socp: solve done, status = {:?}, elapsed = {}.",
346            solution.status,
347            format_duration_human(verboser.elapsed())
348        );
349    }
350    if verboser.is_enabled(Verbosity::Trace) {
351        let show = solution.x.len().min(3);
352        crate::verbosity_log!(
353            crate::diag::Verbosity::Summary,
354            "copp2_socp: solution x_len={}, head={:?}",
355            solution.x.len(),
356            &solution.x[0..show]
357        );
358    }
359    let a_profile = if options.is_allow(solution.status) {
360        Some(clarabel_to_copp2_solution(problem.s_len(), &solution))
361    } else {
362        None
363    };
364    if verboser.is_enabled(Verbosity::Trace) {
365        crate::verbosity_log!(
366            crate::diag::Verbosity::Summary,
367            "copp2_socp: allow(status)={}, extracted_profile={}",
368            options.is_allow(solution.status),
369            if a_profile.is_some() {
370                "Some(a)"
371            } else {
372                "None"
373            }
374        );
375    }
376    Ok(ClarabelExpertInfor2nd {
377        result: a_profile,
378        solution,
379        linsolver,
380    })
381}
382
383/// Determine the number of clarabel's capacity for the objective in COPP2.
384fn clarabel_objective_capacity_copp2<M: RobotBasic>(
385    n: usize,
386    objective: &[CoppObjective],
387    robot: &Robot<M>,
388) -> (usize, usize, usize, usize) {
389    let flag_need_eta = objective.iter().any(|obj| {
390        matches!(
391            obj,
392            CoppObjective::Time(_) | CoppObjective::ThermalEnergy(_, _)
393        )
394    });
395    // Step 1. sqrt(a[k]) >= eta[k] >= 0
396    // num_val <= 4*(n+1), num_b <= 4*(n+1), num_cones <= n+2
397    let (mut capacity_val, mut capacity_b, mut capacity_cones, mut n_vars) = if flag_need_eta {
398        (4 * (n + 1), 4 * (n + 1), n + 2, 2 * (n + 1))
399    } else {
400        (0, 0, 0, n + 1)
401    };
402    // Step 2. objective function
403    let dim = robot.dim();
404    for obj in objective {
405        match obj {
406            CoppObjective::Time(_) => {
407                // num_val <= 6*n, num_b <= 3*n, num_cones <= n, n_var <= n
408                capacity_val += 6 * n;
409                capacity_b += 3 * n;
410                capacity_cones += n;
411                n_vars += n;
412            }
413            CoppObjective::ThermalEnergy(_, _) => {
414                // num_val <= (6+2*dim)*n, num_b <= (dim+2)*n, num_cones <= n, n_var <= n
415                capacity_val += (6 + 2 * dim) * n;
416                capacity_b += (dim + 2) * n;
417                capacity_cones += n;
418                n_vars += n;
419            }
420            CoppObjective::TotalVariationTorque(_, _) => {
421                // num_val <= 8*dim*n, num_b <= 2*dim*n, num_cones <= 1, n_var <= dim*n
422                capacity_val += 8 * dim * n;
423                capacity_b += 2 * dim * n;
424                capacity_cones += 1;
425                n_vars += dim * n;
426            }
427            _ => {}
428        }
429    }
430    (capacity_val, capacity_b, capacity_cones, n_vars)
431}
432
433/// Add the constraints for sqrt(a) >= eta in COPP2 optimization.
434/// x = [a[0], a[1], ..., a[n], eta[0], eta[1], ..., eta[n], ...] \in R^{2*(n+1)+...}.
435/// - interior nodes `k = 1..n-1`: sqrt(a[k]) >= eta[k] >= 0;
436/// - boundary nodes `k = 0, n`: eta[k] = sqrt(a_boundary), since `a[k]` is already fixed.
437///
438/// The boundary nodes must not use the cone `eta^2 <= a` on a fixed `a`. With `a = 0` that cone
439/// has no strictly feasible point: its slack `[0.25, -0.25, eta]` can only sit on the cone
440/// boundary, the matching dual grows without bound as the barrier parameter shrinks, and the
441/// interior-point iterations stall on the primal residual. Fixing `eta[k]` is exact for every
442/// `a_boundary >= 0`, because every objective using `eta` is nonincreasing in it, so
443/// `eta[k] = sqrt(a[k])` at an optimum anyway.
444///
445/// num_val <= 4*(n+1), num_b <= 4*(n+1), num_cones <= n+2
446/// Return the len of the new x: n+1 or 2*(n+1)
447fn clarabel_sqrt_a_copp2(
448    n: usize,
449    objective: &[CoppObjective],
450    a_boundary: (f64, f64),
451    constraints: ConstraintsClarabel,
452) -> usize {
453    let (row, col, val, b, cones) = constraints;
454    for obj in objective {
455        match obj {
456            CoppObjective::Time(_) | CoppObjective::ThermalEnergy(_, _) => {
457                // eta[0] = sqrt(a_start), eta[n] = sqrt(a_final)
458                // A*x-b = -s = 1*eta[k] - sqrt(a_boundary) = 0
459                row.extend(b.len()..b.len() + 2);
460                col.extend([n + 1, 2 * n + 1]);
461                val.extend([1.0, 1.0]);
462                b.extend([a_boundary.0.sqrt(), a_boundary.1.sqrt()]);
463                cones.push(ZeroConeT(2));
464                // eta >= 0 on interior nodes
465                // A*x-b = -s = -1*eta[k] <= 0
466                row.extend(b.len()..b.len() + n - 1);
467                col.extend((n + 2)..(2 * n + 1));
468                val.resize(val.len() + n - 1, -1.0);
469                b.resize(b.len() + n - 1, 0.0);
470                cones.push(NonnegativeConeT(n - 1));
471                // sqrt(a) >= eta on interior nodes
472                // eta^2 <= a
473                // eta^2 + (a - 0.25)^2 <= (a + 0.25)^2
474                // -A*x+b = s = [a+0.25, a-0.25, eta] \in SOC
475                row.extend(b.len()..b.len() + 3 * (n - 1));
476                val.resize(val.len() + 3 * (n - 1), -1.0);
477                cones.resize(cones.len() + n - 1, SecondOrderConeT(3));
478                for k in 1..n {
479                    col.extend([k, k, k + n + 1]);
480                    b.extend([0.25, -0.25, 0.0]);
481                }
482                return 2 * (n + 1);
483            }
484            _ => {}
485        }
486    }
487    n + 1
488}
489
490/// Add the constraints and objective for Time in COPP2 optimization.
491/// num_val <= 6*n, num_b <= 3*n, num_cones <= n, n_var <= n
492fn clarabel_objective_time_copp2(
493    s: &[f64],
494    weight: f64,
495    objective_constraints: ObjConsClarabel,
496) -> bool {
497    if weight < 0.0 {
498        return false;
499    }
500    let (row, col, val, b, cones, q_object) = objective_constraints;
501    // objective: minimize 2 * weight * \sum (s[k+1]-s[k]) / (eta[k] + eta[k+1])
502    // Let: 1 / (eta[k] + eta[k+1]) <= 4 * t[k]
503    // objective: minimize 8 * weight * \sum (s[k+1]-s[k]) * t[k]
504    let weight = 8.0 * weight;
505    let n_var_old = q_object.len();
506    // objective: minimize weight * \sum (s[k+1]-s[k]) * t[k]
507    q_object.extend(s.windows(2).map(|s_pair| weight * (s_pair[1] - s_pair[0])));
508    // t[k] * (eta[k] + eta[k+1]) >= 1
509    // (eta[k] + eta[k+1] + t[k])^2 >= (eta[k] + eta[k+1] - t[k])^2 + 1
510    // -A*x+b = s = [eta[k] + eta[k+1] + t[k], eta[k] + eta[k+1] - t[k], 1] \in SOC
511    let len = s.len();
512    for k in 0..(len - 1) {
513        // eta[k] + eta[k+1] + t[k]
514        row.resize(row.len() + 3, b.len());
515        col.extend([len + k, len + k + 1, n_var_old + k]);
516        val.extend([-1.0, -1.0, -1.0]);
517        b.push(0.0);
518        // eta[k] + eta[k+1] - t[k]
519        row.resize(row.len() + 3, b.len());
520        col.extend([len + k, len + k + 1, n_var_old + k]);
521        val.extend([-1.0, -1.0, 1.0]);
522        b.push(0.0);
523        // 1
524        b.push(1.0);
525    }
526    cones.resize(cones.len() + len - 1, SecondOrderConeT(3));
527    true
528}
529
530/// Add the constraints and objective for ThermalEnergy in COPP2 optimization.
531/// num_val <= (6+2*dim)*n, num_b <= (dim+2)*n, num_cones <= n, n_var <= n
532fn clarabel_objective_thermal_energy_copp2(
533    s: &[f64],
534    weight: f64,
535    normalize: &[f64],
536    coeffs_torque: &(DMatrix<f64>, DMatrix<f64>, DMatrix<f64>),
537    objective_constraints: ObjConsClarabel,
538) -> bool {
539    if weight < 0.0 {
540        return false;
541    }
542    let (row, col, val, b, cones, q_object) = objective_constraints;
543    // minimize: 2 * weight * \sum (s[k+1]-s[k]) / (sqrt(a[k]) + sqrt(a[k+1])) * (tau[i][k] * normalize[i]) ^ 2
544    // minimize: 2 * weight * \sum (s[k+1]-s[k]) / (eta[k] + eta[k+1]) * (tau[i][k] * normalize[i]) ^ 2
545    // Let: \sum_i (tau[i][k] * normalize[i]) ^ 2 / (eta[k] + eta[k+1]) <= 4 * t[k]
546    let len = s.len();
547    let mut coeff_a_curr = coeffs_torque.0.clone();
548    let mut coeff_a_next = coeffs_torque.1.clone();
549    let mut coeff_g = coeffs_torque.2.clone();
550    // objective: minimize 8 * weight * \sum (s[k+1]-s[k]) * t[k]
551    let weight = 8.0 * weight;
552    let n_var_old = q_object.len();
553    // objective: minimize weight * \sum (s[k+1]-s[k]) * t[k]
554    q_object.extend(s.windows(2).map(|s_pair| weight * (s_pair[1] - s_pair[0])));
555    // \sum_i (tau[i][k] * normalize[i]) ^ 2 <= 4 * t[k] * (eta[k] + eta[k+1])
556    let dim = coeff_a_curr.nrows();
557    // tau[i][k] = coeff_a_curr[i][k] * a[k] + coeff_a_next[i][k] * a[k+1] + coeff_g[i][k]
558    if normalize.len() != dim {
559        return false;
560    }
561    let normalize = DVectorView::from_slice(normalize, dim);
562    for mut col in coeff_a_curr.column_iter_mut() {
563        col.component_mul_assign(&normalize);
564    }
565    for mut col in coeff_a_next.column_iter_mut() {
566        col.component_mul_assign(&normalize);
567    }
568    for mut col in coeff_g.column_iter_mut() {
569        col.component_mul_assign(&normalize);
570    }
571    // Now: tau[i][k] * normalize[i] = coeff_a_curr[i][k] * a[k] + coeff_a_next[i][k] * a[k+1] + coeff_g[i][k]
572
573    // (eta[k] + eta[k+1] - t[k])^2 + \sum_i (tau[i][k] * normalize[i]) ^ 2 <= (eta[k] + eta[k+1] + t[k])^2
574    // -A*x+b = s = [eta[k] + eta[k+1] + t[k], eta[k] + eta[k+1] - t[k], tau[0][k] * normalize[0], tau[1][k] * normalize[1], ...] \in SOC
575    for (k, (col_a_curr, col_a_next, col_g)) in izip!(
576        coeff_a_curr.column_iter(),
577        coeff_a_next.column_iter(),
578        coeff_g.column_iter()
579    )
580    .enumerate()
581    {
582        // eta[k] + eta[k+1] + t[k]
583        row.resize(row.len() + 3, b.len());
584        col.extend([len + k, len + k + 1, n_var_old + k]);
585        val.extend([-1.0, -1.0, -1.0]);
586        b.push(0.0);
587        // eta[k] + eta[k+1] - t[k]
588        row.resize(row.len() + 3, b.len());
589        col.extend([len + k, len + k + 1, n_var_old + k]);
590        val.extend([-1.0, -1.0, 1.0]);
591        b.push(0.0);
592        // tau[i][k] * normalize[i] = col_a_curr[i] * x[k] + col_a_next[i] * x[k+1] + col_g[i]
593        for (&v_a_curr, &v_a_next, &v_g) in
594            izip!(col_a_curr.iter(), col_a_next.iter(), col_g.iter())
595        {
596            row.resize(row.len() + 2, b.len());
597            col.extend([k, k + 1]);
598            val.extend([v_a_curr, v_a_next]);
599            b.push(v_g);
600        }
601    }
602    cones.resize(cones.len() + len - 1, SecondOrderConeT(dim + 2));
603    true
604}
605
606/// Add the constraints and objective for TotalVariationTorque in COPP2 optimization.
607/// num_val <= 8*dim*n, num_b <= 2*dim*n, num_cones <= 1, n_var <= dim*n
608fn clarabel_objective_tv_torque_copp2(
609    weight: f64,
610    normalize: &[f64],
611    coeffs_torque: &(DMatrix<f64>, DMatrix<f64>, DMatrix<f64>),
612    objective_constraints: ObjConsClarabel,
613) -> bool {
614    if weight < 0.0 {
615        return false;
616    }
617    let (row, col, val, b, cones, q_object) = objective_constraints;
618    // minimize: weight * \sum |tau[i][k+1]-tau[i][k]| * normalize[i]
619    // Let: |tau[i][k+1]-tau[i][k]| * normalize[i] <= t[i][k]
620    let mut coeff_a_curr = coeffs_torque.0.clone();
621    let mut coeff_a_next = coeffs_torque.1.clone();
622    let mut coeff_g = coeffs_torque.2.clone();
623    let dim = coeff_a_curr.nrows();
624    if normalize.len() != dim {
625        return false;
626    }
627    // tau[i][k] = coeff_a_curr[i][k] * a[k] + coeff_a_next[i][k] * a[k+1] + coeff_g[i][k]
628    let normalize = DVectorView::from_slice(normalize, dim);
629    for mut col in coeff_a_curr.column_iter_mut() {
630        col.component_mul_assign(&normalize);
631    }
632    for mut col in coeff_a_next.column_iter_mut() {
633        col.component_mul_assign(&normalize);
634    }
635    for mut col in coeff_g.column_iter_mut() {
636        col.component_mul_assign(&normalize);
637    }
638    // Now: tau[i][k] * normalize[i] = coeff_a_curr[i][k] * a[k] + coeff_a_next[i][k] * a[k+1] + coeff_g[i][k]
639    // (tau[i][k+1]-tau[i][k]) * normalize[i] = (coeff_a_curr[i][k+1] * a[k+1] + coeff_a_next[i][k+1] * a[k+2] + coeff_g[i][k+1]) - (coeff_a_curr[i][k] * a[k] + coeff_a_next[i][k] * a[k+1] + coeff_g[i][k])
640    // = -coeff_a_curr[i][k] * a[k] + (coeff_a_curr[i][k+1] - coeff_a_next[i][k]) * a[k+1] + coeff_a_next[i][k+1] * a[k+2] + (coeff_g[i][k+1] - coeff_g[i][k])
641
642    let n_b_old = b.len();
643    // A*x-b = -s = -coeff_a_curr[i][k] * a[k] + (coeff_a_curr[i][k+1] - coeff_a_next[i][k]) * a[k+1] + coeff_a_next[i][k+1] * a[k+2] + (coeff_g[i][k+1] - coeff_g[i][k]) - t[i][k] <= 0
644    // A*x-b = -s = coeff_a_curr[i][k] * a[k] - (coeff_a_curr[i][k+1] - coeff_a_next[i][k]) * a[k+1] - coeff_a_next[i][k+1] * a[k+2] - (coeff_g[i][k+1] - coeff_g[i][k]) - t[i][k] <= 0
645    let mut buffer0 = vec![0.0; dim];
646    let mut buffer1 = vec![0.0; dim];
647    for (k, ((col_a_curr, col_b_curr, col_g_curr), (col_a_next, col_b_next, col_g_next))) in izip!(
648        coeff_a_curr.column_iter(),
649        coeff_a_next.column_iter(),
650        coeff_g.column_iter()
651    )
652    .tuple_windows()
653    .enumerate()
654    {
655        // dtau[i] * normalize[i] = -col_a_curr[i] * a[k] + (col_a_next[i] - col_b_curr[i]) * a[k+1] + col_b_next[i] * a[k+2] + (col_g_next[i] - col_g_curr[i])
656        buffer0.clear();
657        buffer1.clear();
658        buffer0.extend(
659            col_a_next
660                .iter()
661                .zip(col_b_curr.iter())
662                .map(|(&v_a_next, &v_b_curr)| v_b_curr - v_a_next),
663        );
664        buffer1.extend(
665            col_g_curr
666                .iter()
667                .zip(col_g_next.iter())
668                .map(|(&v_g_curr, &v_g_next)| v_g_curr - v_g_next),
669        );
670        // dtau[i] * normalize[i] = -col_a_curr[i] * a[k] + buffer0[i] * a[k+1] + col_b_next[i] * a[k+2] + buffer1[i]
671
672        let n_var_old = q_object.len();
673        for (i, (&v0, &v1, &v_a_curr, &v_b_next)) in izip!(
674            buffer0.iter(),
675            buffer1.iter(),
676            col_a_curr.iter(),
677            col_b_next.iter()
678        )
679        .enumerate()
680        {
681            // -col_a_curr[i] * a[k] + buffer0[i] * a[k+1] + col_b_next[i] * a[k+2] + buffer1[i] <= t[i][k]
682            // A*x-b = -s = -col_a_curr[i] * a[k] + buffer0[i] * a[k+1] + col_b_next[i] * a[k+2] + buffer1[i] - t[i][k] <= 0
683            row.resize(row.len() + 4, b.len());
684            col.extend([k, k + 1, k + 2, n_var_old + i]);
685            val.extend([-v_a_curr, v0, v_b_next, -1.0]);
686            b.push(v1);
687
688            // -(-col_a_curr[i] * a[k] + buffer0[i] * a[k+1] + col_b_next[i] * a[k+2]) <= t[i][k]
689            // A*x-b = -s = -(-col_a_curr[i] * a[k] + buffer0[i] * a[k+1] + col_b_next[i] * a[k+2]) - t[i][k] <= 0
690            row.resize(row.len() + 4, b.len());
691            col.extend([k, k + 1, k + 2, n_var_old + i]);
692            val.extend([v_a_curr, -v0, -v_b_next, -1.0]);
693            b.push(-v1);
694        }
695
696        // objective: minimize weight * \sum t[i][k]
697        q_object.resize(q_object.len() + dim, weight);
698    }
699    cones.push(NonnegativeConeT(b.len() - n_b_old));
700    true
701}
702
703/// Add the constraints and objective for Linear in COPP2 optimization.
704fn clarabel_objective_linear_copp2(
705    s: &[f64],
706    weight: f64,
707    alpha: &[f64],
708    beta: &[f64],
709    q_object: &mut [f64],
710) -> bool {
711    if alpha.len() != s.len() || beta.len() != s.len() - 1 {
712        return false;
713    }
714    // objective: minimize weight * \sum (alpha[k]*a[k] + beta[k]*b[k])
715    // weight * \sum alpha[k]*a[k] + 0.5 * beta[k]*(a[k+1]-a[k])/(s[k+1]-s[k])
716    for (va, q) in alpha.iter().zip(q_object.iter_mut()) {
717        // weight * \sum alpha[k]*a[k]
718        *q += weight * va;
719    }
720    for (s_pair, vb, q_curr) in izip!(s.windows(2), beta.iter(), q_object.iter_mut()) {
721        // weight * \sum 0.5 * beta[k]*(a[k+1]-a[k])/(s[k+1]-s[k])
722        *q_curr -= 0.5 * weight * vb / (s_pair[1] - s_pair[0]);
723    }
724    for (s_pair, vb, q_next) in izip!(s.windows(2), beta.iter(), q_object.iter_mut().skip(1)) {
725        // weight * \sum 0.5 * beta[k]*(a[k+1]-a[k])/(s[k+1]-s[k])
726        *q_next += 0.5 * weight * vb / (s_pair[1] - s_pair[0]);
727    }
728    true
729}
730
731fn clarable_objective_copp2<M: RobotTorque>(
732    problem: &Copp2Problem<M>,
733    objective_constraints: ObjConsClarabel,
734) -> Result<(), CoppError> {
735    let (row, col, val, b, cones, q_object) = objective_constraints;
736    let s = problem
737        .robot
738        .constraints
739        .s_vec(problem.idx_s_interval.0, problem.idx_s_interval.1 + 1)?;
740    let coeffs_torque = if problem.objectives.iter().any(|obj| {
741        matches!(
742            obj,
743            CoppObjective::ThermalEnergy(_, _) | CoppObjective::TotalVariationTorque(_, _)
744        )
745    }) {
746        // shape: (dim, n) since there are n+1 a and n b.
747        problem.robot.torque2_coeff_a(
748            problem.idx_s_interval.0,
749            problem.idx_s_interval.1 - problem.idx_s_interval.0,
750        )?
751    } else {
752        (
753            DMatrix::<f64>::zeros(0, 0),
754            DMatrix::<f64>::zeros(0, 0),
755            DMatrix::<f64>::zeros(0, 0),
756        )
757    };
758    for obj in problem.objectives {
759        match obj {
760            CoppObjective::Time(weight) => {
761                if !clarabel_objective_time_copp2(&s, *weight, (row, col, val, b, cones, q_object))
762                {
763                    return Err(CoppError::InvalidInput(
764                        "copp2_socp".into(),
765                        "Invalid Time objective.".into(),
766                    ));
767                }
768            }
769            CoppObjective::ThermalEnergy(weight, normalize) => {
770                if !clarabel_objective_thermal_energy_copp2(
771                    &s,
772                    *weight,
773                    normalize,
774                    &coeffs_torque,
775                    (row, col, val, b, cones, q_object),
776                ) {
777                    return Err(CoppError::InvalidInput(
778                        "copp2_socp".into(),
779                        "Invalid ThermalEnergy objective.".into(),
780                    ));
781                }
782            }
783            CoppObjective::TotalVariationTorque(weight, normalize) => {
784                if !clarabel_objective_tv_torque_copp2(
785                    *weight,
786                    normalize,
787                    &coeffs_torque,
788                    (row, col, val, b, cones, q_object),
789                ) {
790                    return Err(CoppError::InvalidInput(
791                        "copp2_socp".into(),
792                        "Invalid TotalVariationTorque objective.".into(),
793                    ));
794                }
795            }
796            CoppObjective::Linear(weight, alpha, beta) => {
797                if !clarabel_objective_linear_copp2(&s, *weight, alpha, beta, q_object) {
798                    return Err(CoppError::InvalidInput(
799                        "copp2_socp".into(),
800                        "Invalid Linear objective.".into(),
801                    ));
802                }
803            }
804        }
805    }
806    Ok(())
807}
808
809/// Compute the objective value for COPP2 optimization.
810#[cfg(any(feature = "c", feature = "cpp", feature = "python", test))]
811pub(crate) fn objective_value_copp2_opt<M: RobotTorque>(
812    robot: &Robot<M>,
813    start_idx_s: usize,
814    objective: &[CoppObjective],
815    a_profile: &[f64],
816) -> (f64, Vec<f64>) {
817    let Ok(s) = robot
818        .constraints
819        .s_vec(start_idx_s, start_idx_s + a_profile.len())
820    else {
821        return (f64::INFINITY, vec![0.0; objective.len()]);
822    };
823    if a_profile.len() != s.len() {
824        return (f64::INFINITY, vec![0.0; objective.len()]);
825    }
826    let Ok(b_profile) = a_to_b_topp2(&s, a_profile) else {
827        return (f64::INFINITY, vec![0.0; objective.len()]);
828    };
829    let torque = if objective.iter().any(|obj| {
830        matches!(
831            obj,
832            CoppObjective::ThermalEnergy(_, _) | CoppObjective::TotalVariationTorque(_, _)
833        )
834    }) {
835        let torque_result =
836            robot.get_torque_with_ab(&a_profile[0..a_profile.len() - 1], &b_profile, start_idx_s);
837        match torque_result {
838            Ok(torque) => torque,
839            _ => return (f64::INFINITY, vec![0.0; objective.len()]),
840        }
841    } else {
842        DMatrix::<f64>::zeros(0, 0)
843    };
844    let a_sqrt = if objective.iter().any(|obj| {
845        matches!(
846            obj,
847            CoppObjective::Time(_) | CoppObjective::ThermalEnergy(_, _)
848        )
849    }) {
850        a_profile.iter().map(|a| a.sqrt()).collect()
851    } else {
852        Vec::new()
853    };
854    let mut obj_val = Vec::with_capacity(objective.len());
855    let mut obj_val_total = 0.0;
856    for obj in objective {
857        match obj {
858            CoppObjective::Time(weight) => {
859                let obj_here = objective_value_time_copp2(&s, &a_sqrt);
860                obj_val.push(obj_here);
861                obj_val_total += weight * obj_here;
862            }
863            CoppObjective::ThermalEnergy(weight, normalize) => {
864                let obj_here =
865                    objective_value_thermal_energy_copp2(&s, &a_sqrt, &torque, normalize);
866                obj_val.push(obj_here);
867                obj_val_total += weight * obj_here;
868            }
869            CoppObjective::TotalVariationTorque(weight, normalize) => {
870                let obj_here = objective_value_tv_torque_copp2(&torque, normalize);
871                obj_val.push(obj_here);
872                obj_val_total += weight * obj_here;
873            }
874            CoppObjective::Linear(weight, alpha, beta) => {
875                let obj_here = objective_value_linear_copp2(&s, a_profile, alpha, beta);
876                obj_val.push(obj_here);
877                obj_val_total += weight * obj_here;
878            }
879        }
880    }
881    (obj_val_total, obj_val)
882}
883
884/// Compute the time value in COPP2 optimization.
885/// Input: s, a_sqrt = sqrt(a)
886#[cfg(any(feature = "c", feature = "cpp", feature = "python", test))]
887#[inline(always)]
888fn objective_value_time_copp2(s: &[f64], a_sqrt: &[f64]) -> f64 {
889    // objective: minimize 2 * weight * \sum (s[k+1]-s[k]) / (sqrt(a[k]) + sqrt(a[k+1]))
890    let mut objective = 0.0;
891    for (s_pair, a_sqrt_pair) in s.windows(2).zip(a_sqrt.windows(2)) {
892        objective += (s_pair[1] - s_pair[0]) / (a_sqrt_pair[0] + a_sqrt_pair[1]);
893    }
894    2.0 * objective
895}
896
897/// Compute the thermal energy value in COPP2 optimization.
898#[cfg(any(feature = "c", feature = "cpp", feature = "python", test))]
899#[inline(always)]
900fn objective_value_thermal_energy_copp2(
901    s: &[f64],
902    a_sqrt: &[f64],
903    torque: &DMatrix<f64>,
904    normalize: &[f64],
905) -> f64 {
906    // minimize: 2 * \sum (s[k+1]-s[k]) / (sqrt(a[k]) + sqrt(a[k+1])) * (tau[i][k] * normalize[i]) ^ 2
907    let mut objective = 0.0;
908    for (s_pair, a_sqrt_pair, torque_col) in
909        izip!(s.windows(2), a_sqrt.windows(2), torque.column_iter())
910    {
911        let mut sum = 0.0;
912        for (torque, normal) in torque_col.iter().zip(normalize.iter()) {
913            sum += (torque * normal).powi(2);
914        }
915        objective += (s_pair[1] - s_pair[0]) / (a_sqrt_pair[0] + a_sqrt_pair[1]) * sum;
916    }
917    2.0 * objective
918}
919
920/// Compute the total variation of torque value in COPP2 optimization.
921#[cfg(any(feature = "c", feature = "cpp", feature = "python", test))]
922#[inline(always)]
923fn objective_value_tv_torque_copp2(torque: &DMatrix<f64>, normalize: &[f64]) -> f64 {
924    // minimize: weight * \sum |tau[i][k+1]-tau[i][k]| * normalize[i]
925    let mut objective = 0.0;
926    for (torque_col_curr, torque_col_next) in torque.column_iter().tuple_windows() {
927        for (torque_prev, torque_next, normal) in izip!(
928            torque_col_curr.iter(),
929            torque_col_next.iter(),
930            normalize.iter()
931        ) {
932            objective += (torque_next - torque_prev).abs() * normal;
933        }
934    }
935    objective
936}
937
938/// Compute the objective value for Linear in COPP2 optimization.
939#[cfg(any(feature = "c", feature = "cpp", feature = "python", test))]
940#[inline(always)]
941fn objective_value_linear_copp2(s: &[f64], a_profile: &[f64], alpha: &[f64], beta: &[f64]) -> f64 {
942    // objective: minimize \sum (alpha[k]*a[k] + beta[k]*b[k])
943    // \sum alpha[k]*a[k] + 0.5*beta[k]*(a[k+1]-a[k])/(s[k+1]-s[k])
944    let mut objective = 0.0;
945    for (a_curr, alpha_curr) in a_profile.iter().zip(alpha.iter()) {
946        // alpha[k]*a[k]
947        objective += a_curr * alpha_curr;
948    }
949    for (a_pair, s_pair, beta_curr) in izip!(a_profile.windows(2), s.windows(2), beta.iter()) {
950        // 0.5*beta[k]*(a[k+1]-a[k])/(s[k+1]-s[k])
951        objective += 0.5 * beta_curr * (a_pair[1] - a_pair[0]) / (s_pair[1] - s_pair[0]);
952    }
953    objective
954}
955
956#[cfg(test)]
957mod tests {
958    use super::*;
959    use crate::copp::copp2::stable::basic::{
960        Copp2ProblemBuilder, Topp2ProblemBuilder, s_to_t_topp2,
961    };
962    use crate::copp::copp2::stable::reach_set2::{ReachSet2Options, ReachSet2OptionsBuilder};
963    use crate::copp::copp2::stable::topp2_ra::topp2_ra;
964    use crate::copp::{ClarabelOptions, ClarabelOptionsBuilder};
965    use crate::path::{
966        Path, SplineConfig, add_symmetric_axial_limits_for_test, lissajous_path_for_test,
967    };
968    use crate::robot::demo::Plannar2LinkEnd;
969    use crate::robot::robot_core::Robot;
970    use core::panic;
971    use nalgebra::DMatrix;
972    use std::time::Instant;
973    use std::vec;
974
975    #[test]
976    fn test_copp2_socp_only_time() -> Result<(), CoppError> {
977        run_test_copp2_socp_only_time_repeated(1, false)
978    }
979
980    /// Conditions: release, --include-ignored, CPU = Intel(R) Core(TM) Ultra 9 285K.
981    /// Average 100 experiments: tc_ra = 0.2005 ms, tc_lp = 29.7361 ms, tc_qp = 166.5122 ms, tf_ra = 4.766745, tf_lp = 4.766745, tf_qp = 4.766735
982    #[test]
983    #[ignore = "slow"]
984    fn test_copp2_socp_only_time_robust() -> Result<(), CoppError> {
985        run_test_copp2_socp_only_time_repeated(100, true)?;
986        Ok(())
987    }
988
989    #[test]
990    fn test_copp2_socp() -> Result<(), CoppError> {
991        let options_socp = ClarabelOptionsBuilder::new()
992            .allow_almost_solved(true)
993            .build()?;
994        run_test_copp2_socp_once(&options_socp)
995    }
996
997    /// Regression: the first-order rows were shifted by one row, which dropped `a[n-1] >= 0`
998    /// and turned the first finite `amax` row into `a[k] <= amax[k] + a[n-1]`. A Linear
999    /// objective adds no SOC row that would re-impose `a >= 0`.
1000    #[test]
1001    fn test_copp2_socp_linear_respects_amax() -> Result<(), CoppError> {
1002        let n: usize = 6;
1003        let s: Vec<f64> = (0..n).map(|k| k as f64).collect();
1004        let amax = vec![1.0; n];
1005        let mut robot = Robot::with_capacity(1usize, n);
1006        robot.with_s(&s)?;
1007        robot.constraints.with_constraint_1order(&amax, 0)?;
1008
1009        // Maximize sum(a) between two boundary states at full speed, a = amax = 1.
1010        let alpha = vec![-1.0; n];
1011        let beta = vec![0.0; n - 1];
1012        let objectives = [CoppObjective::Linear(1.0, &alpha, &beta)];
1013        let problem =
1014            Copp2ProblemBuilder::new(&robot, (0, n - 1), (1.0, 1.0), &objectives).build()?;
1015        let options = ClarabelOptionsBuilder::new()
1016            .allow_almost_solved(true)
1017            .build()?;
1018        let a = copp2_socp(&problem, &options)?;
1019
1020        for (k, (&a_k, &amax_k)) in a.iter().zip(&amax).enumerate() {
1021            assert!(
1022                a_k <= amax_k + 1e-6,
1023                "a[{k}] = {a_k} exceeds amax[{k}] = {amax_k}"
1024            );
1025        }
1026        Ok(())
1027    }
1028
1029    /// Regression: with `a[0] = a[n] = 0`, the boundary cones `eta^2 <= a` admitted no strictly
1030    /// feasible point. Clarabel then stalled on the primal residual, and bit-level changes of the
1031    /// path decided between `AlmostSolved`, `InsufficientProgress` and `NumericalError`.
1032    #[test]
1033    fn test_copp2_socp_zero_boundary_converges() -> Result<(), CoppError> {
1034        use crate::path::{Jet3, sin};
1035        use std::f64::consts::PI;
1036
1037        const DIM: usize = 3;
1038        let n: usize = 1001;
1039        let s: Vec<f64> = (0..n).map(|j| j as f64 / (n - 1) as f64).collect();
1040        let limit_max = [1.0; DIM];
1041        let limit_min = [-1.0; DIM];
1042        let normalize = [1.0; DIM];
1043        let objectives = [
1044            CoppObjective::Time(1.0),
1045            CoppObjective::ThermalEnergy(0.1, &normalize),
1046        ];
1047        let options = ClarabelOptionsBuilder::new()
1048            .allow_almost_solved(true)
1049            .build()?;
1050        for k in 0..4 {
1051            let dphi = k as f64 * 1e-15;
1052            let path = Path::from_parametric(
1053                move |s: Jet3| {
1054                    vec![
1055                        sin(2.0 * PI * s + dphi),
1056                        sin(3.0 * PI * s + (0.3 + dphi)),
1057                        sin(5.0 * PI * s + (0.7 + dphi)),
1058                    ]
1059                },
1060                0.0,
1061                1.0,
1062            )?;
1063            let mut robot = Robot::with_capacity(DIM, n);
1064            robot
1065                .with_s(s.as_slice())?
1066                .with_q_from_path_2nd(&path, 0, n)?
1067                .with_axial_velocity((limit_max.as_slice(), n), (limit_min.as_slice(), n), 0)?
1068                .with_axial_acceleration((limit_max.as_slice(), n), (limit_min.as_slice(), n), 0)?;
1069            let problem =
1070                Copp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0), &objectives).build()?;
1071            let (a, solution) = copp2_socp_expert(&problem, &options)?;
1072            assert!(
1073                a.is_some() && solution.r_prim <= 1e-6,
1074                "dphi = {dphi:e}: status = {:?}, iterations = {}, r_prim = {:e}",
1075                solution.status,
1076                solution.iterations,
1077                solution.r_prim
1078            );
1079        }
1080        Ok(())
1081    }
1082
1083    #[test]
1084    #[ignore = "bindings"]
1085    fn test_copp2_socp_bindings_parity() -> Result<(), CoppError> {
1086        let dim = 3;
1087        let num_waypoints = 8;
1088        let n: usize = 81;
1089        let pi = std::f64::consts::PI;
1090
1091        let waypoints = DMatrix::<f64>::from_fn(dim, num_waypoints, |axis, j| {
1092            let s = j as f64 / (num_waypoints - 1) as f64;
1093            match axis {
1094                0 => 0.20 * (2.0 * pi * s).sin(),
1095                1 => 0.15 * (1.5 * pi * s).cos(),
1096                2 => 0.10 * s * (1.0 - s),
1097                _ => unreachable!("dimension is fixed to 3"),
1098            }
1099        });
1100        let path = Path::from_waypoints_interpolating(&waypoints, SplineConfig::default())?;
1101        let s = DMatrix::<f64>::from_fn(1, n, |_, j| j as f64 / (n - 1) as f64);
1102
1103        let mut robot = Robot::with_capacity(dim, n);
1104        robot
1105            .with_s(&s.as_view())?
1106            .with_q_from_path_2nd(&path, 0, n)?;
1107        add_symmetric_axial_limits_for_test(&mut robot, 10.0, 50.0, None)?;
1108        let torque_max = vec![1.0e6; dim];
1109        let torque_min = vec![-1.0e6; dim];
1110        robot.with_axial_torque((torque_max.as_slice(), n), (torque_min.as_slice(), n), 0)?;
1111
1112        let normalize = vec![1.0; dim];
1113        let options = ClarabelOptionsBuilder::new()
1114            .allow_almost_solved(true)
1115            .build()?;
1116
1117        let objectives_thermal = [
1118            CoppObjective::Time(1.0),
1119            CoppObjective::ThermalEnergy(1.0, &normalize),
1120        ];
1121        let problem_thermal =
1122            Copp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0), &objectives_thermal)
1123                .build()?;
1124        let a_thermal = copp2_socp(&problem_thermal, &options)?;
1125        let (t_final_thermal, _) = s_to_t_topp2(s.as_slice(), &a_thermal, 0.0)?;
1126
1127        let objectives_tv = [
1128            CoppObjective::Time(1.0),
1129            CoppObjective::TotalVariationTorque(1.0, &normalize),
1130        ];
1131        let problem_tv =
1132            Copp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0), &objectives_tv).build()?;
1133        let a_tv = copp2_socp(&problem_tv, &options)?;
1134
1135        crate::verbosity_log!(
1136            crate::diag::Verbosity::Summary,
1137            "COPP2-SOCP Rust bindings parity test: t_final_thermal={:.17}, thermal.len={}, tv.len={}",
1138            t_final_thermal,
1139            a_thermal.len(),
1140            a_tv.len()
1141        );
1142
1143        Ok(())
1144    }
1145
1146    /// Conditions: release, --include-ignored, CPU = Intel(R) Core(TM) Ultra 9 285K.
1147    /// Average 100 experiments:
1148    //  Case 0: tc=161.691ms, obj=[4.824561313613876, 2576.8873693126284, 71.69242263342399, -1.048938713665848e-14]
1149    //  Case 1: tc=200.959ms, obj=[4.830121022357044, 2576.928260526994, 66.14588916597324, -7.651101974204267e-15]
1150    //  Case 2: tc=218.223ms, obj=[4.830230167437296, 2576.9529624501106, 66.12305109242398, -1.09470765785602e-14]
1151    //  Case 3: tc=93.588ms, obj=[361.45110144118144, 194870.1172166501, 48.05756488144189, 9.743247875171334e-16]
1152    //  Case 4: tc=164.505ms, obj=[4.824561266838617, 2576.887346690997, 71.69241993761281, 2.7478852526741092e-14]
1153    #[test]
1154    #[ignore = "slow"]
1155    fn test_copp2_socp_robust() -> Result<(), CoppError> {
1156        run_test_copp2_socp_repeated(100, true)?;
1157        Ok(())
1158    }
1159
1160    fn run_test_copp2_socp_repeated(n_exp: usize, flag_print_step: bool) -> Result<(), CoppError> {
1161        let options_socp = ClarabelOptionsBuilder::new()
1162            .allow_almost_solved(true)
1163            .build()?;
1164
1165        let mut tc_sum_case0 = 0.0;
1166        let mut tc_sum_case1 = 0.0;
1167        let mut tc_sum_case2 = 0.0;
1168        let mut tc_sum_case3 = 0.0;
1169        let mut tc_sum_case4 = 0.0;
1170        let mut obj_sum_case0 = vec![0.0; 4];
1171        let mut obj_sum_case1 = vec![0.0; 4];
1172        let mut obj_sum_case2 = vec![0.0; 4];
1173        let mut obj_sum_case3 = vec![0.0; 4];
1174        let mut obj_sum_case4 = vec![0.0; 4];
1175
1176        for i_exp in 0..n_exp {
1177            let n: usize = 1000;
1178            let mut robot = Robot::with_capacity(Plannar2LinkEnd::new(1.0, 1.0, 1.0, 1.0), n);
1179            let dim = robot.dim();
1180
1181            let mut rng = rand::rng();
1182            let (s, path, omega, phi) =
1183                lissajous_path_for_test(dim, n, &mut rng).expect("random range is valid");
1184            robot
1185                .with_s(&s.as_view())?
1186                .with_q_from_path_2nd(&path, 0, n)?;
1187            add_symmetric_axial_limits_for_test(&mut robot, 1.0, 1.0, None)?;
1188
1189            // Test different objectives in COPP2 optimization
1190            let objectives_test = [
1191                CoppObjective::Time(1.0),
1192                CoppObjective::ThermalEnergy(1.0, &vec![1.0; dim]),
1193                CoppObjective::TotalVariationTorque(1.0, &vec![1.0; dim]),
1194                CoppObjective::Linear(1.0, &vec![0.0; n], &vec![1.0; n - 1]),
1195            ];
1196            let a_feasible = vec![0.0; n];
1197
1198            // Case 0: Time only
1199            let mut copp2_problem = Copp2ProblemBuilder::new(
1200                &robot,
1201                (0, n - 1),
1202                (0.0, 0.0),
1203                &[CoppObjective::Time(1.0)],
1204            )
1205            .build()?;
1206            let start = Instant::now();
1207            let mut a_case0 = copp2_socp(&copp2_problem, &options_socp)?;
1208            let tc_copp2_case0 = start.elapsed().as_secs_f64() * 1E3;
1209            robot
1210                .constraints
1211                .project_to_feasible_topp2(&mut a_case0, &a_feasible, 0)?;
1212            let (_, obj_case0) = objective_value_copp2_opt(&robot, 0, &objectives_test, &a_case0);
1213
1214            // Case 1: Time and ThermalEnergy
1215            let obj_case1 = [
1216                CoppObjective::Time(1.0),
1217                CoppObjective::ThermalEnergy(1.0, &vec![1.0; dim]),
1218            ];
1219            copp2_problem.objectives = &obj_case1;
1220            let start = Instant::now();
1221            let mut a_case1 = copp2_socp(&copp2_problem, &options_socp)?;
1222            let tc_copp2_case1 = start.elapsed().as_secs_f64() * 1E3;
1223            robot
1224                .constraints
1225                .project_to_feasible_topp2(&mut a_case1, &a_feasible, 0)?;
1226            let (_, obj_case1) = objective_value_copp2_opt(&robot, 0, &objectives_test, &a_case1);
1227            if obj_case1[0] < obj_case0[0] - 1E-3 || obj_case1[1] - 1E-3 > obj_case0[1] {
1228                let (tf_case0, _) = s_to_t_topp2(s.as_slice(), &a_case0, 0.0)?;
1229                let (tf_case1, _) = s_to_t_topp2(s.as_slice(), &a_case1, 0.0)?;
1230                crate::verbosity_log!(
1231                    crate::diag::Verbosity::Summary,
1232                    "omega = {omega:?}\nphi = {phi:?}"
1233                );
1234                crate::verbosity_log!(
1235                    crate::diag::Verbosity::Summary,
1236                    "Case 0: obj_time = {:.6}, obj_thermal_energy = {:.6}, tf = {:.6}",
1237                    obj_case0[0],
1238                    obj_case0[1],
1239                    tf_case0
1240                );
1241                crate::verbosity_log!(
1242                    crate::diag::Verbosity::Summary,
1243                    "Case 1: obj_time = {:.6}, obj_thermal_energy = {:.6}, tf = {:.6}",
1244                    obj_case1[0],
1245                    obj_case1[1],
1246                    tf_case1
1247                );
1248                crate::verbosity_log!(
1249                    crate::diag::Verbosity::Summary,
1250                    "Interesting... Cases 0 and 1"
1251                );
1252            }
1253
1254            // Case 2: Time and More ThermalEnergy
1255            let obj_case2 = [
1256                CoppObjective::Time(1.0),
1257                CoppObjective::ThermalEnergy(10.0, &vec![1.0; dim]),
1258            ];
1259            copp2_problem.objectives = &obj_case2;
1260            let start = Instant::now();
1261            let mut a_case2 = copp2_socp(&copp2_problem, &options_socp)?;
1262            let tc_copp2_case2 = start.elapsed().as_secs_f64() * 1E3;
1263            robot
1264                .constraints
1265                .project_to_feasible_topp2(&mut a_case2, &a_feasible, 0)?;
1266            let (_, obj_case2) = objective_value_copp2_opt(&robot, 0, &objectives_test, &a_case2);
1267            if obj_case2[0] < obj_case1[0] - 1E-3 || obj_case2[1] - 1E-3 > obj_case1[1] {
1268                crate::verbosity_log!(
1269                    crate::diag::Verbosity::Summary,
1270                    "omega = {omega:?}\nphi = {phi:?}"
1271                );
1272                crate::verbosity_log!(
1273                    crate::diag::Verbosity::Summary,
1274                    "Case 1: obj_time = {:.6}, obj_thermal_energy = {:.6}",
1275                    obj_case1[0],
1276                    obj_case1[1]
1277                );
1278                crate::verbosity_log!(
1279                    crate::diag::Verbosity::Summary,
1280                    "Case 2: obj_time = {:.6}, obj_thermal_energy = {:.6}",
1281                    obj_case2[0],
1282                    obj_case2[1]
1283                );
1284                crate::verbosity_log!(
1285                    crate::diag::Verbosity::Summary,
1286                    "Interesting... Cases 1 and 2"
1287                );
1288            }
1289
1290            // Case 3: Time and TotalVariationTorque
1291            let obj_case3 = [
1292                CoppObjective::Time(1.0),
1293                CoppObjective::TotalVariationTorque(1.0, &vec![1.0; dim]),
1294            ];
1295            copp2_problem.objectives = &obj_case3;
1296            let start = Instant::now();
1297            let mut a_case3 = copp2_socp(&copp2_problem, &options_socp)?;
1298            let tc_copp2_case3 = start.elapsed().as_secs_f64() * 1E3;
1299            robot
1300                .constraints
1301                .project_to_feasible_topp2(&mut a_case3, &a_feasible, 0)?;
1302            let (_, obj_case3) = objective_value_copp2_opt(&robot, 0, &objectives_test, &a_case3);
1303            if obj_case3[1] < obj_case1[1] - 1E-3 || obj_case3[2] - 1E-3 > obj_case1[2] {
1304                crate::verbosity_log!(
1305                    crate::diag::Verbosity::Summary,
1306                    "omega = {omega:?}\nphi = {phi:?}"
1307                );
1308                crate::verbosity_log!(
1309                    crate::diag::Verbosity::Summary,
1310                    "Case 1: obj_time = {:.6}, obj_thermal_energy = {:.6}, obj_total_variation_torque = {:.6}",
1311                    obj_case1[0],
1312                    obj_case1[1],
1313                    obj_case1[2]
1314                );
1315                crate::verbosity_log!(
1316                    crate::diag::Verbosity::Summary,
1317                    "Case 3: obj_time = {:.6}, obj_thermal_energy = {:.6}, obj_total_variation_torque = {:.6}",
1318                    obj_case3[0],
1319                    obj_case3[1],
1320                    obj_case3[2]
1321                );
1322                crate::verbosity_log!(
1323                    crate::diag::Verbosity::Summary,
1324                    "Interesting... Cases 1 and 3"
1325                );
1326            }
1327
1328            // Case 4: Time and Linear
1329            let obj_case4 = [
1330                CoppObjective::Time(1.0),
1331                CoppObjective::Linear(1.0, &vec![0.0; n], &vec![1.0; n - 1]),
1332            ];
1333            copp2_problem.objectives = &obj_case4;
1334            let start = Instant::now();
1335            let mut a_case4 = copp2_socp(&copp2_problem, &options_socp)?;
1336            let tc_copp2_case4 = start.elapsed().as_secs_f64() * 1E3;
1337            robot
1338                .constraints
1339                .project_to_feasible_topp2(&mut a_case4, &a_feasible, 0)?;
1340            let (_, obj_case4) = objective_value_copp2_opt(&robot, 0, &objectives_test, &a_case4);
1341            if obj_case4[1] < obj_case1[1] - 1E-3 || obj_case4[3] - 1E-3 > obj_case1[3] {
1342                crate::verbosity_log!(
1343                    crate::diag::Verbosity::Summary,
1344                    "omega = {omega:?}\nphi = {phi:?}"
1345                );
1346                crate::verbosity_log!(
1347                    crate::diag::Verbosity::Summary,
1348                    "Case 1: obj_time = {:.6}, obj_thermal_energy = {:.6}, obj_linear = {:.6}",
1349                    obj_case1[0],
1350                    obj_case1[1],
1351                    obj_case1[3]
1352                );
1353                crate::verbosity_log!(
1354                    crate::diag::Verbosity::Summary,
1355                    "Case 4: obj_time = {:.6}, obj_thermal_energy = {:.6}, obj_linear = {:.6}",
1356                    obj_case4[0],
1357                    obj_case4[1],
1358                    obj_case4[3]
1359                );
1360                crate::verbosity_log!(
1361                    crate::diag::Verbosity::Summary,
1362                    "Interesting... Cases 1 and 4"
1363                );
1364            }
1365            if obj_case4[2] < obj_case2[2] - 1E-3 || obj_case4[3] - 1E-3 > obj_case2[3] {
1366                crate::verbosity_log!(
1367                    crate::diag::Verbosity::Summary,
1368                    "omega = {omega:?}\nphi = {phi:?}"
1369                );
1370                crate::verbosity_log!(
1371                    crate::diag::Verbosity::Summary,
1372                    "Case 2: obj_time = {:.6}, obj_total_variation_torque = {:.6}, obj_linear = {:.6}",
1373                    obj_case2[0],
1374                    obj_case2[2],
1375                    obj_case2[3]
1376                );
1377                crate::verbosity_log!(
1378                    crate::diag::Verbosity::Summary,
1379                    "Case 4: obj_time = {:.6}, obj_total_variation_torque = {:.6}, obj_linear = {:.6}",
1380                    obj_case4[0],
1381                    obj_case4[2],
1382                    obj_case4[3]
1383                );
1384                crate::verbosity_log!(
1385                    crate::diag::Verbosity::Summary,
1386                    "Interesting... Cases 2 and 4"
1387                );
1388            }
1389
1390            if flag_print_step {
1391                crate::verbosity_log!(
1392                    crate::diag::Verbosity::Summary,
1393                    "Exp #{}:\n Case 0: tc={:.3}ms, obj={:?}\n Case 1: tc={:.3}ms, obj={:?}\n Case 2: tc={:.3}ms, obj={:?}\n Case 3: tc={:.3}ms, obj={:?}\n Case 4: tc={:.3}ms, obj={:?}",
1394                    i_exp + 1,
1395                    tc_copp2_case0,
1396                    obj_case0,
1397                    tc_copp2_case1,
1398                    obj_case1,
1399                    tc_copp2_case2,
1400                    obj_case2,
1401                    tc_copp2_case3,
1402                    obj_case3,
1403                    tc_copp2_case4,
1404                    obj_case4
1405                );
1406            }
1407
1408            tc_sum_case0 += tc_copp2_case0;
1409            tc_sum_case1 += tc_copp2_case1;
1410            tc_sum_case2 += tc_copp2_case2;
1411            tc_sum_case3 += tc_copp2_case3;
1412            tc_sum_case4 += tc_copp2_case4;
1413            for i in 0..obj_case0.len() {
1414                obj_sum_case0[i] += obj_case0[i];
1415                obj_sum_case1[i] += obj_case1[i];
1416                obj_sum_case2[i] += obj_case2[i];
1417                obj_sum_case3[i] += obj_case3[i];
1418                obj_sum_case4[i] += obj_case4[i];
1419            }
1420        }
1421
1422        for i in 0..4 {
1423            obj_sum_case0[i] /= n_exp as f64;
1424            obj_sum_case1[i] /= n_exp as f64;
1425            obj_sum_case2[i] /= n_exp as f64;
1426            obj_sum_case3[i] /= n_exp as f64;
1427            obj_sum_case4[i] /= n_exp as f64;
1428        }
1429
1430        crate::verbosity_log!(
1431            crate::diag::Verbosity::Summary,
1432            "Average {} experiments:\n Case 0: tc={:.3}ms, obj={:?}\n Case 1: tc={:.3}ms, obj={:?}\n Case 2: tc={:.3}ms, obj={:?}\n Case 3: tc={:.3}ms, obj={:?}\n Case 4: tc={:.3}ms, obj={:?}",
1433            n_exp,
1434            tc_sum_case0 / n_exp as f64,
1435            obj_sum_case0,
1436            tc_sum_case1 / n_exp as f64,
1437            obj_sum_case1,
1438            tc_sum_case2 / n_exp as f64,
1439            obj_sum_case2,
1440            tc_sum_case3 / n_exp as f64,
1441            obj_sum_case3,
1442            tc_sum_case4 / n_exp as f64,
1443            obj_sum_case4
1444        );
1445
1446        Ok(())
1447    }
1448
1449    fn run_test_copp2_socp_once(_options_socp: &ClarabelOptions) -> Result<(), CoppError> {
1450        run_test_copp2_socp_repeated(1, false)
1451    }
1452
1453    fn run_one_copp2_socp_only_time_case(
1454        options_ra: &ReachSet2Options,
1455        options_socp: &ClarabelOptions,
1456    ) -> Result<(f64, f64, f64, f64, f64, f64), CoppError> {
1457        let n: usize = 1000;
1458        let mut robot = Robot::with_capacity(Plannar2LinkEnd::new(1.0, 1.0, 1.0, 1.0), n);
1459        let dim = robot.dim();
1460
1461        let mut rng = rand::rng();
1462        let (s, path, omega, phi) =
1463            lissajous_path_for_test(dim, n, &mut rng).expect("random range is valid");
1464        robot
1465            .with_s(&s.as_view())?
1466            .with_q_from_path_2nd(&path, 0, n)?;
1467        add_symmetric_axial_limits_for_test(&mut robot, 1.0, 1.0, None)?;
1468
1469        let topp2_problem = Topp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0)).build()?;
1470        let start = Instant::now();
1471        let a_ra = topp2_ra(&topp2_problem, options_ra)?;
1472        let tc_ra = start.elapsed().as_secs_f64() * 1E3;
1473        let (tf_ra, _) = s_to_t_topp2(s.as_slice(), &a_ra, 0.0)?;
1474
1475        let obj1 = [CoppObjective::Linear(
1476            1.0,
1477            &vec![-1.0; n],
1478            &vec![0.0; n - 1],
1479        )];
1480        let mut copp2_problem =
1481            Copp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0), &obj1).build()?;
1482        let start = Instant::now();
1483        let a_lp = copp2_socp(&copp2_problem, options_socp)?;
1484        let tc_lp = start.elapsed().as_secs_f64() * 1E3;
1485        let (tf_lp, _) = s_to_t_topp2(s.as_slice(), &a_lp, 0.0)?;
1486
1487        copp2_problem.objectives = &[CoppObjective::Time(1.0)];
1488        let start = Instant::now();
1489        let a_qp = copp2_socp(&copp2_problem, options_socp)?;
1490        let tc_qp = start.elapsed().as_secs_f64() * 1E3;
1491        let (tf_qp, _) = s_to_t_topp2(s.as_slice(), &a_qp, 0.0)?;
1492
1493        if (tf_lp - tf_ra).abs() > 1e-3 || (tf_qp - tf_ra).abs() > 1e-3 {
1494            crate::verbosity_log!(
1495                crate::diag::Verbosity::Summary,
1496                "omega = {omega:?}\nphi = {phi:?}"
1497            );
1498            panic!("COPP2 time optimality failed!");
1499        }
1500
1501        Ok((tc_ra, tc_lp, tc_qp, tf_ra, tf_lp, tf_qp))
1502    }
1503
1504    fn run_test_copp2_socp_only_time_repeated(
1505        n_exp: usize,
1506        flag_print_step: bool,
1507    ) -> Result<(), CoppError> {
1508        let mut tc_sum_ra = 0.0;
1509        let mut tc_sum_lp = 0.0;
1510        let mut tc_sum_qp = 0.0;
1511        let mut tf_sum_ra = 0.0;
1512        let mut tf_sum_lp = 0.0;
1513        let mut tf_sum_qp = 0.0;
1514
1515        let options_ra = ReachSet2OptionsBuilder::new()
1516            .lp_feas_tol(1E-9)
1517            .a_cmp_abs_tol(1E-9)
1518            .a_cmp_rel_tol(1E-9)
1519            .build()?;
1520        let options_socp = ClarabelOptionsBuilder::new()
1521            .allow_almost_solved(true)
1522            .build()?;
1523
1524        for i_exp in 0..n_exp {
1525            let (tc_ra, tc_lp, tc_qp, tf_ra, tf_lp, tf_qp) =
1526                run_one_copp2_socp_only_time_case(&options_ra, &options_socp)?;
1527
1528            if flag_print_step {
1529                crate::verbosity_log!(
1530                    crate::diag::Verbosity::Summary,
1531                    "Exp #{}: tc_ra = {:.4} ms, tc_lp = {:.4} ms, tc_qp = {:.4} ms, tf_ra = {:.6}, tf_lp = {:.6}, tf_qp = {:.6}",
1532                    i_exp + 1,
1533                    tc_ra,
1534                    tc_lp,
1535                    tc_qp,
1536                    tf_ra,
1537                    tf_lp,
1538                    tf_qp,
1539                );
1540            }
1541
1542            tc_sum_ra += tc_ra;
1543            tc_sum_lp += tc_lp;
1544            tc_sum_qp += tc_qp;
1545            tf_sum_ra += tf_ra;
1546            tf_sum_lp += tf_lp;
1547            tf_sum_qp += tf_qp;
1548        }
1549
1550        crate::verbosity_log!(
1551            crate::diag::Verbosity::Summary,
1552            "Average {} experiments: tc_ra = {:.4} ms, tc_lp = {:.4} ms, tc_qp = {:.4} ms, tf_ra = {:.6}, tf_lp = {:.6}, tf_qp = {:.6}",
1553            n_exp,
1554            tc_sum_ra / n_exp as f64,
1555            tc_sum_lp / n_exp as f64,
1556            tc_sum_qp / n_exp as f64,
1557            tf_sum_ra / n_exp as f64,
1558            tf_sum_lp / n_exp as f64,
1559            tf_sum_qp / n_exp as f64
1560        );
1561
1562        Ok(())
1563    }
1564}